// Copyright (C) 2005-2008 Jae Stutzman (jaebird@gmail.com)
//
// This file is part of SoundConsole.
//
// SoundConsole is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// SoundConsole is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with SoundConsole.  If not, see <http://www.gnu.org/licenses/>.

//#define USE_WAV_FILE

using System;
using System.IO;
using System.Threading;
using Microsoft.DirectX;
using Microsoft.DirectX.DirectSound;

namespace SoundConsole
{
	public class SoundCapture : IDisposable
	{
		public const int NumberRecordNotifications	= 16;

		private BufferPositionNotify[] positionNotify = new BufferPositionNotify[NumberRecordNotifications + 1];  
		private AutoResetEvent notificationEvent	= null;
		private CaptureBuffer applicationBuffer = null;
		private Guid captureDeviceGuid = Guid.Empty;
		private Capture applicationDevice = null;
#if USE_WAV_FILE
		private string fileName = @"c:\temp\test.wav"; //string.Empty;
		private FileStream waveFile = null;
		private BinaryWriter writer = null;
		private string Path = string.Empty;
		private bool Recording = false;
#else
		private Stream stdOutStream = Console.OpenStandardOutput();
#endif
		private Notify applicationNotify = null;
		private Thread notifyThread = null;
		private int captureBufferSize = 0;
		private int nextCaptureOffset = 0;
		private WaveFormat inputFormat = new WaveFormat();
		private int sampleCount = 0;
		private int notifySize = 0;
		private bool capturing = false;

		public bool Capturing
		{
			get
			{
				if (applicationBuffer != null)
				{
					return applicationBuffer.Capturing;
				}
				else
					return false;
			}
		}

		/// <summary>
		/// Default ctor does not initialize anything.
		/// </summary>
		public SoundCapture() {}

		public SoundCapture(short bits, short channels, int rate, TimeSpan duration, int deviceIndex)
		{
			captureDeviceGuid = GetDeviceGuid(deviceIndex);

			InitDirectSound();

			inputFormat.SamplesPerSecond = rate;
			inputFormat.BitsPerSample = bits;
			inputFormat.Channels = channels;
			inputFormat.BlockAlign = (short)(inputFormat.Channels * (inputFormat.BitsPerSample / 8));
			inputFormat.AverageBytesPerSecond = inputFormat.BlockAlign * inputFormat.SamplesPerSecond;

			//CreateCaptureBuffer();

		}

		/// <summary>
		/// Start capture and push to stdout.
		/// </summary>
		public void Start()
		{
#if USE_WAV_FILE
			CreateRIFF();
#endif
			// get the party started...
			StartOrStopRecord(true);
		}

		/// <summary>
		/// Stop capture and stop stream to stdout.
		/// </summary>
		public void Stop()
		{
			StartOrStopRecord(false);
		}

		/// <summary>
		/// Enumerate by string the capture devices in the system.
		/// </summary>
		/// <returns></returns>
		public string[] GetCaptureDevices()
		{
			CaptureDevicesCollection devices = new CaptureDevicesCollection();

			string[] strDevices = new string[devices.Count];
			int index = 0;

			foreach (DeviceInformation info in devices)
			{
				strDevices[index] = index.ToString() + ": " + info.Description;
				index++;
			}

			return strDevices;
		}

		/// <summary>
		/// Get the GUID of the device specified
		/// </summary>
		/// <param name="deviceIndex"></param>
		/// <returns></returns>
		public Guid GetDeviceGuid(int deviceIndex)
		{
			CaptureDevicesCollection devices = new CaptureDevicesCollection();

			if (deviceIndex < devices.Count && deviceIndex > -1)
			{
				return devices[deviceIndex].DriverGuid;
			}
			else
			{
				return devices[0].DriverGuid;
			}
		}

		/// <summary>
		/// Initialize the DirectSound subsystem
		/// </summary>
		private void InitDirectSound()
		{
			captureBufferSize = 0;
			notifySize = 0;

			// Create DirectSound.Capture using the preferred capture device
			applicationDevice = new Capture(captureDeviceGuid);
		}

		/// <summary>
		/// Creates a capture buffer and sets the format
		/// </summary>
		void CreateCaptureBuffer()
		{
			CaptureBufferDescription dscheckboxd = new CaptureBufferDescription(); 

			if (null != applicationNotify)
			{
				applicationNotify.Dispose();
				applicationNotify = null;
			}
			if (null != applicationBuffer)
			{
				applicationBuffer.Dispose();
				applicationBuffer = null;
			}

			if (0 == inputFormat.Channels)
				return;

			// Set the notification size
			notifySize = (1024 > inputFormat.AverageBytesPerSecond / 8) ? 1024 : (inputFormat.AverageBytesPerSecond / 8);
			notifySize -= notifySize % inputFormat.BlockAlign;   

			// Set the buffer sizes
			captureBufferSize = notifySize * NumberRecordNotifications;

			// Create the capture buffer
			dscheckboxd.BufferBytes = captureBufferSize;
			inputFormat.FormatTag = WaveFormatTag.Pcm;
			dscheckboxd.Format = inputFormat; // Set the format during creatation
		
			applicationBuffer = new CaptureBuffer(dscheckboxd, applicationDevice);
			nextCaptureOffset = 0;

			InitNotifications();
		}

		/// <summary>
		/// Inits the notifications on the capture buffer which are handled
		/// in the notify thread.
		/// </summary>
		void InitNotifications()
		{
			if (null == applicationBuffer)
				throw new NullReferenceException();
		
			// Create a thread to monitor the notify events
			if (null == notifyThread)
			{
				// Create a notification event, for when the sound stops playing
				notificationEvent = new AutoResetEvent(false);

				notifyThread = new Thread(new ThreadStart(WaitThread));
				capturing = true;
				notifyThread.Start();
			}


			// Setup the notification positions
			for (int i = 0; i < NumberRecordNotifications; i++)
			{
				positionNotify[i].Offset = (notifySize * i) + notifySize - 1;
				positionNotify[i].EventNotifyHandle = notificationEvent.Handle;
			}
		
			applicationNotify = new Notify(applicationBuffer);

			// Tell DirectSound when to notify the app. The notification will come in the from 
			// of signaled events that are handled in the notify thread.
			applicationNotify.SetNotificationPositions(positionNotify, NumberRecordNotifications);
		}

		/// <summary>
		/// Entry method for thread which waits for messages to arrive.
		/// </summary>
		private void WaitThread()
		{
			while(capturing)
			{
				//Sit here and wait for a message to arrive
				notificationEvent.WaitOne(Timeout.Infinite, true);
				RecordCapturedData();
			}
		}

		/// <summary>
		/// Copies data from the capture buffer to the output buffer
		/// </summary>
		void RecordCapturedData() 
		{
			byte[] CaptureData = null;
			int ReadPos;
			int CapturePos;
			int LockSize;

			applicationBuffer.GetCurrentPosition(out CapturePos, out ReadPos);
			LockSize = ReadPos - nextCaptureOffset;
			if (LockSize < 0)
				LockSize += captureBufferSize;

			// Block align lock size so that we are always write on a boundary
			LockSize -= (LockSize % notifySize);

			if (0 == LockSize)
				return;

			// Read the capture buffer.
			CaptureData = (byte[])applicationBuffer.Read(nextCaptureOffset, typeof(byte), LockFlag.None, LockSize);

#if USE_WAV_FILE
			// Write the data into the wav file
			writer.Write(CaptureData, 0, CaptureData.Length);
#else
			stdOutStream.Write(CaptureData, 0, CaptureData.Length);
#endif	
			// Update the number of samples, in bytes, of the file so far.
			sampleCount += CaptureData.Length;

			// Move the capture offset along
			nextCaptureOffset += CaptureData.Length; 
			nextCaptureOffset %= captureBufferSize; // Circular buffer
		}

		/// <summary>
		/// Starts or stops the capture buffer from recording
		/// </summary>
		/// <param name="StartRecording"></param>
		void StartOrStopRecord(bool StartRecording)
		{
			if (StartRecording)
			{
				// Create a capture buffer, and tell the capture 
				// buffer to start recording   
				CreateCaptureBuffer();
				applicationBuffer.Start(true);
			}
			else
			{
				// Stop the buffer, and read any data that was not 
				// caught by a notification
				applicationBuffer.Stop();

				RecordCapturedData();
#if USE_WAV_FILE
				writer.Seek(4, SeekOrigin.Begin); // Seek to the length descriptor of the RIFF file.
				writer.Write((int)(sampleCount + 36));	// Write the file length, minus first 8 bytes of RIFF description.
				writer.Seek(40, SeekOrigin.Begin); // Seek to the data length descriptor of the RIFF file.
				writer.Write(sampleCount); // Write the length of the sample data in bytes.
			
				writer.Close();	// Close the file now.
				writer = null;	// Set the writer to null.
				waveFile = null; // Set the FileStream to null.
#endif
			}
		}

#if USE_WAV_FILE
		private void CreateSoundFile(string filename)
		{
			//-----------------------------------------------------------------------------
			// Name: OnCreateSoundFile()
			// Desc: Called when the user requests to save to a sound file
			//-----------------------------------------------------------------------------

			WaveFormat wf = new WaveFormat();

			// Get the default media path (something like C:\WINDOWS\MEDIA)
			if (string.Empty == Path)
				Path = Environment.CurrentDirectory.Substring(0, Environment.CurrentDirectory.LastIndexOf("\\")) + "\\media";

			if (Recording)
			{
				// Stop the capture and read any data that 
				// was not caught by a notification
				StartOrStopRecord(false);
				Recording = false;
			}

			CreateRIFF();

			// Remember the path for next time
			Path = fileName.Substring(0, fileName.LastIndexOf("\\"));
		}

		void CreateRIFF()
		{
			/**************************************************************************
			 
				Here is where the file will be created. A
				wave file is a RIFF file, which has chunks
				of data that describe what the file contains.
				A wave RIFF file is put together like this:
			 
				The 12 byte RIFF chunk is constructed like this:
				Bytes 0 - 3 :	'R' 'I' 'F' 'F'
				Bytes 4 - 7 :	Length of file, minus the first 8 bytes of the RIFF description.
								(4 bytes for "WAVE" + 24 bytes for format chunk length +
								8 bytes for data chunk description + actual sample data size.)
				Bytes 8 - 11:	'W' 'A' 'V' 'E'
			
				The 24 byte FORMAT chunk is constructed like this:
				Bytes 0 - 3 :	'f' 'm' 't' ' '
				Bytes 4 - 7 :	The format chunk length. This is always 16.
				Bytes 8 - 9 :	File padding. Always 1.
				Bytes 10- 11:	Number of channels. Either 1 for mono,  or 2 for stereo.
				Bytes 12- 15:	Sample rate.
				Bytes 16- 19:	Number of bytes per second.
				Bytes 20- 21:	Bytes per sample. 1 for 8 bit mono, 2 for 8 bit stereo or
								16 bit mono, 4 for 16 bit stereo.
				Bytes 22- 23:	Number of bits per sample.
			
				The DATA chunk is constructed like this:
				Bytes 0 - 3 :	'd' 'a' 't' 'a'
				Bytes 4 - 7 :	Length of data, in bytes.
				Bytes 8 -...:	Actual sample data.
			
			***************************************************************************/

			// Open up the wave file for writing.
			waveFile = new FileStream(fileName, FileMode.Create);
			writer = new BinaryWriter(waveFile);

			// Set up file with RIFF chunk info.
			char[] ChunkRiff = {'R','I','F','F'};
			char[] ChunkType = {'W','A','V','E'};
			char[] ChunkFmt	= {'f','m','t',' '};
			char[] ChunkData = {'d','a','t','a'};
			
			short shPad = 1; // File padding
			int nFormatChunkLength = 0x10; // Format chunk length.
			int nLength = 0; // File length, minus first 8 bytes of RIFF description. This will be filled in later.
			short shBytesPerSample = 0; // Bytes per sample.

			// Figure out how many bytes there will be per sample.
			if (8 == inputFormat.BitsPerSample && 1 == inputFormat.Channels)
				shBytesPerSample = 1;
			else if ((8 == inputFormat.BitsPerSample && 2 == inputFormat.Channels) || (16 == inputFormat.BitsPerSample && 1 == inputFormat.Channels))
				shBytesPerSample = 2;
			else if (16 == inputFormat.BitsPerSample && 2 == inputFormat.Channels)
				shBytesPerSample = 4;

			// Fill in the riff info for the wave file.
			writer.Write(ChunkRiff);
			writer.Write(nLength);
			writer.Write(ChunkType);

			// Fill in the format info for the wave file.
			writer.Write(ChunkFmt);
			writer.Write(nFormatChunkLength);
			writer.Write(shPad);
			writer.Write(inputFormat.Channels);
			writer.Write(inputFormat.SamplesPerSecond);
			writer.Write(inputFormat.AverageBytesPerSecond);
			writer.Write(shBytesPerSample);
			writer.Write(inputFormat.BitsPerSample);
			
			// Now fill in the data chunk.
			writer.Write(ChunkData);
			writer.Write((int)0);	// The sample length will be written in later.
		}
#endif

		#region IDisposable Members

		public void Dispose()
		{
			StartOrStopRecord(false);
		}

		#endregion
	}
}
